//-----------------------------------------------------------------------------------------------
// Author: Andre Van Der Merwe <dart@pobox.com>
// Project: .net Compound File (http://dotnetpcf.sourceforge.net)
//
// Modifications
//    2001-02-14: (AVDM) Added OpenFile(...) to CFFiles
//    2001-02-18: (AVDM) Added CFFile.PropFileID
//    2001-02-18: (AVDM) CFDirector.DeleteFile & DeleteDirectory now also delete property streams
//    2001-02-23: (AVDM) Misc changes - lower coupling in all units
// 
// Copyright (c) Andre Van Der Merwe 2000-2001
// Licence: BSD - see the licence.txt file or visit the project homepage
//-----------------------------------------------------------------------------------------------

//TODO both file and dir collections need to be updated on add/del of dir/file in cf
//TODO change all fields to private/protected, internal access via properties/methods only
//TODO check that where ever nessasary asserts are followed by if statements for release mode
//TODO is there a better way of getting the next ID for items (i.e. not using the nast while loops)?
//UPGRADE_TODO need a list of open files, can't del open file etc
//UPGRADE_TODO thread safe
//UPGRADE_TODO file shrink code - remove extra alloc blocks + empty trailing blocks
//UPGRADE_TODO file defrag code - defrag, create as new file (same way COM doc files work)?
//UPGRADE_TODO import/export to COM compound file
//UPGRADE_TODO allow creating of a CF on any stream

namespace dotnetpcf.CompoundFile
{
   using System;
   using System.Diagnostics;
   using System.Collections;
   using System.Data;
   using System.IO;

   /// <summary>
   /// User level class that provides additional user friendly functionality. This is that class 
   /// that all users ofthe compound file should be creating. See CompoundFileSys for more 
   /// system level information.
   /// 
   /// This class adds another layer over the system level functionality. For instance it exposes the 
   /// CFDirector and CFDirectories classes which make navigating and working with the directories 
   /// very user friendly. The user of this class should have no access to the underlying classes and
   /// should not need to know about them to be able to use this class.
   /// </summary>
   public class CompoundFile : CompoundFileSys
   {
      public CompoundFile() : base()
      {
      }

      /// <summary>
      /// Open an existing compound file
      /// </summary>
      /// <param name="sFileName">Path and file name</param>
      public override CompoundFileResult OpenFile(  string sFileName  ) 
      {
         CompoundFileResult cfr = base.OpenFile(  sFileName  );

         if(  CompoundFileResult.cfrOk != cfr  )
            return cfr;
         
         m_CurrentDir = new CFDirectory(  this,  null  );
         return CompoundFileResult.cfrOk;
      }

      /// <summary>
      /// Open an existing compound file
      /// </summary>
      /// <param name="sFileName">Path and file name</param>
      public override CompoundFileResult OpenFile(Stream stream)
      {
          CompoundFileResult cfr = base.OpenFile(stream);

          if (CompoundFileResult.cfrOk != cfr)
              return cfr;

          m_CurrentDir = new CFDirectory(this, null);
          return CompoundFileResult.cfrOk;
      }

      public static string TestVer()
      {
         return "0006"; //TODO remove TestVer()
      }


      //-----------
      // Directory 
      //-----------

      private CFDirectory m_CurrentDir = null;

      /// <summary>
      /// Set the root as the working directory
      /// </summary>
      public void MoveToRootDirectory()
      {
         m_CurrentDir = new CFDirectory(  this,  null  );
      }

      /// <summary>
      /// Return the current working directory
      /// </summary>
      public CFDirectory WorkingDirectory
      {
         get{ return m_CurrentDir; }
         set
         { 
            Debug.Assert(  null != value,  "trying to set current dir to null"  );

            if(  null != value  )
               m_CurrentDir = value;
         }
      }
   }

   /// <summary>
   /// User level class that provides access to a file 
   /// </summary>
   public class CFFile
   {
      internal CFFile(  CompoundFile cf,  FileTable.FileTableEntry fent  )
      {
         Debug.Assert(  null != cf,  "cant initialize CFFile with a null compound file"  );
         Debug.Assert(  null != cf,  "cant initialize CFFile with a null file table entry"  );

         m_fent = fent;
         m_cfParent = cf;
      }

      private CFDirectory m_dirParent = null;
      private CompoundFile m_cfParent = null;
      private FSFilterStream m_PropFile = null;
      private FileTable.FileTableEntry m_fent = null;
      
      public FSFilterStream OpenFile()
      {
         return new FSFilterStream(  m_cfParent.CompoundFileStream,  m_fent  );
      }

      public uint ID
      {
         get{ return m_fent.m_iID; }
      }

      public string Name
      {
         get{ return m_fent.Name; }
         set{ m_fent.Name = value; }
      }

      public FileFlags Flags
      {
         get{ return (FileFlags)m_fent.m_iFlags; }
      }

      public uint Length
      {
         get{ return m_fent.m_iLength; }
      }

      public CFDirectory ParentDirectory
      {
         get
         { 
            if(  null == m_dirParent  )
            {
               m_dirParent = new CFDirectory(  m_cfParent,  m_fent.m_iDirID  );
               return m_dirParent;
            }

            return m_dirParent;
         }
      }

      public bool HasPropertyFile
      {
         get{ return (SysInfo.UNDEFINED != m_fent.m_iPropID ); }
      }

      internal uint PropFileID
      {
         get{ return m_fent.m_iPropID; }
      }

      protected FSFilterStream CreatePropertyFile()
      {
         FileTable.FileTableEntry fent;

         if(  SysInfo.UNDEFINED == m_fent.m_iPropID  )
         {
            fent = m_cfParent.FileTable.CreateNewFile(  AllocTableFlags.efFileProperty | AllocTableFlags.efSystem,  
                                                          FileFlags.ffFileProperty | FileFlags.ffSystem,  
                                                          SysInfo.FILENAME_PROPFILE  
                                                        );

            m_fent.m_iPropID = fent.m_iID;
         }
         else
            fent = m_cfParent.FileTable.GetFileTableEntryByID(  m_fent.m_iPropID  );

         m_PropFile = new FSFilterStream(  m_cfParent.CompoundFileStream,  fent  );

         return m_PropFile;
      }

      public FSFilterStream PropFile
      {
         get{ return ((null != m_PropFile) ? m_PropFile : CreatePropertyFile()); }
      }
   }


   public class CFFiles : IEnumerable
   {
      public class CFFileEnumerator : IEnumerator
      {
         internal CFFileEnumerator(  CFFiles files  )
         {
            m_iIndex = -1;
            m_Files = files;
         }

         private int m_iIndex;
         private CFFiles m_Files;
         
         public bool MoveNext()
         {
            return (++m_iIndex >= m_Files.Length) ? false : true;
         }

         public void Reset()
         {
            m_iIndex = -1;
         }

         object IEnumerator.Current
         {
            get { return m_Files[ m_iIndex ]; }
         }

         public CFFile Current
         {
            get { return (CFFile)m_Files[ m_iIndex ]; }
         }

         internal void FileAdded(  uint iFileID  )
         {
            m_Files.FileAdded(  iFileID  );
         }
      }

      //------

      internal CFFiles(  CompoundFile cf,  CFDirectory dirParent  )
      {
         Debug.Assert(  null != dirParent,  "cant create CFFiles with a null parent dir"  );

         m_cfParent = cf;
         m_dirParent = dirParent;

         IDictionaryEnumerator elems = m_cfParent.FileTable.GetEntryEnumerator();

         while(  elems.MoveNext()  )
         {
            FileTable.FileTableEntry fent = ((FileTable.FileTableEntry)elems.Value);

            if(   m_dirParent.ID == fent.m_iDirID   )
            {
                  //Ignore all system files
               if(  0 != ((uint)FileFlags.ffSystem & fent.m_iFlags)  )
                  continue;

               m_FileIDs.Add(  fent.m_iID  );
            }
         }
      }

      private CFDirectory m_dirParent = null;
      private ArrayList m_FileIDs = new ArrayList();
      private CompoundFile m_cfParent = null;

      public int Length
      {
         get{ return m_FileIDs.Count; }
      }

      public IEnumerator GetEnumerator()
      {
         return ((IEnumerator)new CFFileEnumerator(  this  ));
      }

      public CFFile this[ int iIndex ]
      {
         get
         { 
            uint iFileID = (uint)m_FileIDs[ iIndex ];
            FileTable.FileTableEntry dent = m_cfParent.FileTable.GetFileTableEntryByID(  iFileID  );
            return new CFFile(  m_cfParent,  dent  );
         }
      }

      internal void FileAdded(  uint iFileID  )
      {
         if(  !m_FileIDs.Contains(  iFileID  )  )
            m_FileIDs.Add(  iFileID  );
      }
   }


   //---------------------


   public class CFDirectories : IEnumerable
   {
      public class CFDirectoryEnumerator : IEnumerator
      {
         internal CFDirectoryEnumerator(  CFDirectories dir  )
         {
            m_iIndex = -1;
            m_Directories = dir;
         }

         private int m_iIndex;
         private CFDirectories m_Directories;
         
         public bool MoveNext()
         {
            return (++m_iIndex >= m_Directories.Length) ? false : true;
         }

         public void Reset()
         {
            m_iIndex = -1;
         }

         object IEnumerator.Current
         {
            get{ return m_Directories[ m_iIndex ]; }
         }

         public CFDirectory Current
         {
            get{ return (CFDirectory)m_Directories[ m_iIndex ]; }
         }
      }

      //------

      internal CFDirectories(  CompoundFile cf,  CFDirectory dirParent  )
      {
         Debug.Assert(  null != dirParent,  "cant create CFDirectories with a null parent"  );

         m_cfParent = cf;
         m_dirParent = dirParent;

         IDictionaryEnumerator elems = m_cfParent.DirTable.GetEntryEnumerator();

         while(  elems.MoveNext()  )
         {
            if(   m_dirParent.ID == ((DirTable.DirTableEntry)elems.Value).m_iParentID   )
               m_DirIDs.Add(  ((DirTable.DirTableEntry)elems.Value).m_iID  );
         }
      }

      private CFDirectory m_dirParent = null;
      private ArrayList m_DirIDs = new ArrayList();
      private CompoundFile m_cfParent = null;

      public int Length
      {
         get{ return m_DirIDs.Count; }
      }

      public IEnumerator GetEnumerator()
      {
         return ((IEnumerator)new CFDirectoryEnumerator(  this  ));
      }

      public CFDirectory this[ int iIndex ]
      {
         get
         { 
            uint iDirID = (uint)m_DirIDs[ iIndex ];
            DirTable.DirTableEntry dent = m_cfParent.DirTable.GetDirTableEntryByID(  iDirID  );
            return new CFDirectory(  m_cfParent,  dent  );
         }
      }
   }


   /// <summary>
   /// User level class that provides access to a directory
   /// </summary>
   public class CFDirectory
   {
      /// <summary>
      /// 
      /// </summary>
      /// <param name="cf">compound file</param>
      /// <param name="dent">this dir entry or null for root</param>
      internal CFDirectory(  CompoundFile cf,  DirTable.DirTableEntry dent  )
      {
         Init(  cf,  dent  );
      }

      internal CFDirectory(  CompoundFile cf,  uint dentID  )
      {
         DirTable.DirTableEntry dent = cf.DirTable.GetDirTableEntryByID(  dentID  );
         Init(  cf,  dent  );         
      }

      private void Init(  CompoundFile cf,  DirTable.DirTableEntry dent  )
      {
         Debug.Assert(  null != cf,  "cant initialize CFDirectory with a null compound file"  );
         m_dent = dent;
         m_cfParent = cf;
         m_Files = new CFFiles(  m_cfParent,  this  );
      }

      private CFFiles m_Files = null;
      private CompoundFile m_cfParent = null;
      private DirTable.DirTableEntry m_dent = null;
      private FSFilterStream m_PropFile = null;


      public CFDirectory CreateDirectory(  string sName  )
      {
         if(   !m_cfParent.DirTable.CanSetDirName(  sName,  m_dent,  null  )   )
         {
            Debug.Write(  "Dir with this name already exists"  );
            return null;
         }

         DirTable.DirTableEntry dent = m_cfParent.DirTable.CreateDirectory(  sName,  m_dent  );
         dent.m_iParentID = ID;
         
         return new CFDirectory(  m_cfParent,  dent  );
      }

      public CFFile CreateFile(  string sName  )
      {
            //check that file with this name does not already exist
         if(   !m_cfParent.FileTable.CanSetFileName(  sName,  ID,  null  )   )
         {
            Debug.Write(  "File with this name already exists"  );
            return null;
         }

         FileTable.FileTableEntry fent = m_cfParent.FileTable.CreateNewFile(  sName  );
         fent.m_iDirID = ID;

            //Add new file to the list
         m_Files.FileAdded(  fent.m_iID  );

         return new CFFile(  m_cfParent,  fent  );
      }

      public CFFile OpenFileByName(  string sName  )
      {
         foreach(  CFFile file in Files  )
         {
            if(  sName == file.Name  )
               return file;
         }

         return null;
      }

      public bool DeleteFile(  string sName  )
      {
         foreach(  CFFile file in Files  )
         {
            if(  sName == file.Name  )
            {
               if(  file.HasPropertyFile  )
                  DeleteFile(  file.PropFileID  );
               
               return DeleteFile(  file.ID  );
            }
               
         }

         return false;
      }

      public bool DeleteFile(  uint iID  )
      {
         FileTable.FileTableEntry fent = m_cfParent.FileTable.GetFileTableEntryByID(  iID  );

            //Can only delete a file if it is in this directory
         if(  fent.m_iDirID != ID  )
         {
            Debug.Write(  "can only delete files in the current directory"  );
            return false;
         }

         return m_cfParent.FileTable.DeleteFileEntry(  iID  );
      }

      public bool DeleteDirectory(  string sName  )
      {
         foreach(  CFDirectory dir in SubDirectories  )
         {
            if(  sName == dir.Name  )
               return DeleteDirectory(  dir.ID  );
         }

         return false;
      }

      public bool DeleteDirectory(  uint iID  )
      {
         DirTable.DirTableEntry dent = m_cfParent.DirTable.GetDirTableEntryByID(  iID  );
         
         if(  dent.m_iParentID != ID  )
         {
            Debug.Write(  "Can only delete sub-dirs"  );
            return false;
         }

         if(  SysInfo.UNDEFINED != dent.m_iPropID  )
            m_cfParent.DirTable.DeleteDirEntry(  dent.m_iPropID  );

         return m_cfParent.DirTable.DeleteDirEntry(  iID  );
      }

      public uint ID
      {
         get{ return ((null != m_dent) ? m_dent.m_iID : SysInfo.UNDEFINED); }
      }

      public DirFlags Flags
      {
         get{ return ((null != m_dent) ? (DirFlags)m_dent.m_iFlags : 0); }
      }

      public string Name
      {
         get{ return ((null != m_dent) ? m_dent.Name : ""); }
         set
         { 
            if(  null != m_dent  )
               m_dent.Name = value; 
         }
      }

      public CFDirectory ParentDirectory
      {
         get
         {
            if(  null == m_dent)
               return null;

            DirTable.DirTableEntry dent = m_cfParent.DirTable.GetDirTableEntryByID(  m_dent.m_iParentID  );
            return new CFDirectory(  m_cfParent,  dent  );
         }
      }

      public CFDirectories SubDirectories
      {
         get{  return new CFDirectories(  m_cfParent,  this  );  }
      }

      public CFFiles Files
      {
         //TODO change to return new CFFiles? - slow?
         get{ return m_Files; }
      }

      public bool HasPropFile
      {
         get{ return (null != m_PropFile); }
      }

      public FSFilterStream CreatePropertyFile()
      {
         FileTable.FileTableEntry fent;

         if(  SysInfo.UNDEFINED == m_dent.m_iPropID  )
         {
            fent = m_cfParent.FileTable.CreateNewFile(  AllocTableFlags.efDirProperty | AllocTableFlags.efSystem,  
                                                        FileFlags.ffDirProperty | FileFlags.ffSystem,  
                                                        SysInfo.FILENAME_PROPFILE  
                                                      );

            m_dent.m_iPropID = fent.m_iID;
         }
         else
            fent = m_cfParent.FileTable.GetFileTableEntryByID(  m_dent.m_iPropID  );

         m_PropFile = new FSFilterStream(  m_cfParent.CompoundFileStream,  fent  );

         return m_PropFile;
      }

      public FSFilterStream PropFile
      {
         get{ return ((null != m_PropFile) ? m_PropFile : CreatePropertyFile()); }
      }
   }

}
